Skip to content

Conversation

shaavan
Copy link
Member

@shaavan shaavan commented Jun 7, 2025

Builds on #3964

This PR addresses two current limitations in the LDK offer-handling flow:

  1. InvoiceRequest messages cannot be intercepted, inspected, or handled manually before responding.
  2. Offers denominated in currencies other than msats are not supported.

To solve this, we introduce a new FlowEvents interface that enables asynchronous handling of InvoiceRequests, allowing developers to inspect, validate, or delay invoice generation as needed.

We also parameterize the invoice-building flow with a CurrencyConversion trait, enabling developers to inject custom conversion logic and support offers denominated in fiat or other currencies. Developers can also supply a custom payment hash if needed.

Together, these enhancements give developers greater control and flexibility over how invoices are constructed and how offers are processed—especially in use cases involving multiple currencies or asynchronous workflows.

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jun 7, 2025

👋 Thanks for assigning @jkczyz as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@shaavan
Copy link
Member Author

shaavan commented Jun 7, 2025

cc @jkczyz

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@joostjager
Copy link
Contributor

Is this proposed change a response to a request from a specific user/users?

@shaavan
Copy link
Member Author

shaavan commented Jun 11, 2025

Hi @joostjager!

This PR is actually a continuation of the original thread that led to the OffersMessageFlow: link to thread.

The motivation behind it was to provide users with the ability to handle InvoiceRequests asynchronously—just like we already allow for Bolt12Invoices. However, adding more events into the middle of the ChannelManager flow felt suboptimal.

So, as a first step, we worked on refactoring most of the Offers-related code out of ChannelManager into the new OffersMessageFlow (#3639). Now that the refactor is complete, this PR picks up the original goal again: to let users asynchronously handle both InvoiceRequests and Invoices. This not only gives them more flexibility in analyzing these Offer messages, but also opens the door for creating custom interfaces—for example, to support Offers in different currency denominations.

Hope that gives a clear picture of the intent behind this! Let me know if you have any thoughts or suggestions—would love to hear them. Thanks a lot!

@jkczyz
Copy link
Contributor

jkczyz commented Jun 11, 2025

Another use case is Fedimint, where they'll want to include their own payment hash in the Bolt12Invoice.

@valentinewallace
Copy link
Contributor

Another use case is Fedimint, where they'll want to include their own payment hash in the Bolt12Invoice.

Does Fedimint plan to use the OffersMessageFlow without a ChannelManager?

@jkczyz
Copy link
Contributor

jkczyz commented Jun 11, 2025

Does Fedimint plan to use the OffersMessageFlow without a ChannelManager?

I believe with one.

@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 3rd Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 4th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 5th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 6th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 7th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 8th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 9th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 10th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 11th Reminder

Hey @joostjager! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@jkczyz jkczyz removed the request for review from joostjager July 2, 2025 13:38
@jkczyz
Copy link
Contributor

jkczyz commented Jul 2, 2025

Removing @joostjager for now to stop bot spam. @shaavan and I have been working through some variations of this approach.

Copy link
Contributor

@vincenzopalazzo vincenzopalazzo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK for me

I was just looking around to sync with this Offer Flow

@shaavan shaavan changed the title Introduce Event Model for Offers Flow Introduce Synchronous Currency Conversion Support in Offers Aug 2, 2025
Copy link

codecov bot commented Aug 2, 2025

Codecov Report

❌ Patch coverage is 88.34951% with 60 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.77%. Comparing base (0d64c86) to head (1ea58db).
⚠️ Report is 63 commits behind head on main.

Files with missing lines Patch % Lines
lightning/src/offers/flow.rs 83.80% 16 Missing and 7 partials ⚠️
lightning/src/offers/invoice_request.rs 86.17% 13 Missing ⚠️
lightning/src/ln/channelmanager.rs 82.35% 9 Missing and 3 partials ⚠️
lightning/src/offers/invoice.rs 94.56% 3 Missing and 2 partials ⚠️
lightning/src/offers/offer.rs 71.42% 4 Missing ⚠️
lightning/src/ln/offers_tests.rs 96.87% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3833      +/-   ##
==========================================
+ Coverage   88.60%   88.77%   +0.17%     
==========================================
  Files         180      180              
  Lines      134878   136847    +1969     
  Branches   134878   136847    +1969     
==========================================
+ Hits       119511   121492    +1981     
+ Misses      12608    12547      -61     
- Partials     2759     2808      +49     
Flag Coverage Δ
fuzzing 20.96% <2.31%> (-0.81%) ⬇️
tests 88.62% <87.76%> (+0.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shaavan shaavan changed the title Introduce Synchronous Currency Conversion Support in Offers Support Currency-Based Offers and Async Invoice Handling via FlowEvents Aug 22, 2025
// In this case we must enforce the Offer's minimum (if any):
// reject if the IR's amount is below the Offer-implied floor.
if let Some(ir_msats) = inner.amount_msats() {
if offer_msats_opt.map_or(false, |offer_msats| ir_msats < offer_msats) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be true if the offer doesn't have an amount?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually a failure condition:

if offer_msats_opt.map_or(false, |offer_msats| ir_msats < offer_msats) {
    return Err(Bolt12SemanticError::InsufficientAmount);
}

So if the offer doesn’t specify an amount, any ir_msats is accepted, which matches the “donation” case. That’s why I treat this here as valid rather than failing.

Let me know if I’m missing something in this reasoning.

@jkczyz
Copy link
Contributor

jkczyz commented Sep 15, 2025

Removed some comments that were meant for #3964.

@shaavan
Copy link
Member Author

shaavan commented Sep 18, 2025

Updated from pr3833.03 to pr3833.04 (diff)
Addressed @jkczyz comments

Changes:

  • Fix documentation.
  • Code cleanup. Moved the conversion function as an implementation on Amount.
  • Renamed FlowEvents -> OfferMessageFlowEvent.

Comment on lines +12511 to +12685
if let Some(Amount::Currency { iso4217_code: _, amount: _ }) = offer.amount() {
if amount_msats.is_none() {
return Err(Bolt12SemanticError::MissingAmount);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic error: The validation checks if amount_msats.is_none() for currency offers, but this validation occurs before the invoice request is created. At this point, amount_msats represents the user-provided amount parameter, not the final computed amount from the invoice request. Currency offers may be valid without an explicit amount if the offer itself specifies the amount. This will incorrectly reject valid currency-based offers.

Suggested change
if let Some(Amount::Currency { iso4217_code: _, amount: _ }) = offer.amount() {
if amount_msats.is_none() {
return Err(Bolt12SemanticError::MissingAmount);
}
}
if let Some(Amount::Currency { iso4217_code: _, amount }) = offer.amount() {
if amount.is_none() && amount_msats.is_none() {
return Err(Bolt12SemanticError::MissingAmount);
}
}

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@shaavan
Copy link
Member Author

shaavan commented Sep 19, 2025

Updated from pr3833.04 to pr3833.05 (diff)

  • Rebase on main.

In the following commits we will introduce `fields` function
for other types as well, so to keep code DRY we convert the
function to a macro.
This commit reintroduces `VerifiedInvoiceRequest`, now parameterized by
`SigningPubkeyStrategy`.

The key motivation is to restrict which functions can be called on a
`VerifiedInvoiceRequest` based on its strategy type. This enables
compile-time guarantees — ensuring that an incorrect `InvoiceBuilder`
cannot be constructed for a given request, and misuses are caught early.
This change improves type safety and architectural clarity
by introducing dedicated `InvoiceBuilder` methods tied to
each variant of `VerifiedInvoiceRequestEnum`.

With this change, users are now required to match on the
enum variant before calling the corresponding builder method.
This pushes the responsibility of selecting the correct
builder to the user and ensures that invalid builder
usage is caught at compile time, rather than relying
on runtime checks.

The signing logic has also been moved from the builder
to the `ChannelManager`. This shift simplifies the
builder's role and aligns it with the rest of the API,
where builder methods return a configurable object that
can be extended before signing. The result is a more
consistent and predictable interface that separates
concerns cleanly and makes future maintenance easier.
To ensure correct Bolt12 payment flow behavior, the `amount_msats`
used for generating the `payment_hash`, `payment_secret`,
and payment path must remain consistent. Previously, these steps
could inadvertently diverge due to separate sources of `amount_msats`.

This commit refactors the interface to use a `get_payment_info` closure,
which captures the required variables and provides a single source of
truth for both payment info (payment_hash, payment_secret) and path
generation. This ensures consistency and eliminates subtle bugs
that could arise from mismatched amounts across the flow.
Adds the `CurrencyConversion` trait to allow users to define custom
logic for converting fiat amounts into millisatoshis (msat).

This abstraction lays the groundwork for supporting Offers denominated
in fiat currencies, where conversion is inherently context-dependent.
This commit updates the Bolt12Invoice amount creation logic to utilize
the `CurrencyConversion` trait, enabling more flexible and customizable
handling of fiat-to-msat conversions.

Reasoning

The `CurrencyConversion` trait is passed upstream into the invoice's amount
creation flow, where it is used to interpret the Offer’s currency amount
(if present) into millisatoshis.

This change establishes a unified mechanism for amount handling—regardless
of whether the Offer’s amount is denominated in Bitcoin or fiat, or whether
the InvoiceRequest specifies an amount or not.
We introduce this check in pay_for_offer, to ensure that
if the offer amount is specified in currency, a corresponding amount
to be used in invoice request must be provided.

**Reasoning:** When responding to an offer with currency, we enforce
that the invoice request must always include an amount. This ensures we
never receive an invoice tied to a currency-denominated offer without
a corresponding request amount.

By moving currency conversion upfront into the invoice request creation
where the user can supply their own conversion logic — we avoid pushing
conversion concerns into invoice parsing. This significantly reduces
complexity during invoice verification.
Previously, the `enqueue_invoice` function in the `Flow` component
accepted a `Refund` as input and dispatched the invoice either directly
to a known `PublicKey` or via `BlindedMessagePath`s, depending on what
was available within the `Refund`.

While this worked for the refund-based flow, it tightly coupled invoice
dispatch logic to the `Refund` abstraction, limiting its general
usability outside of that context.

The upcoming commits will introduce support for constructing and
enqueuing invoices from manually handled `InvoiceRequest`s—decoupled
from the `Refund` flow. To enable this, we are preemptively introducing
more flexible, destination-specific variants of the enqueue function.

Specifically, the `Flow` now exposes two dedicated methods:

- `enqueue_invoice_using_node_id`: For sending an invoice directly to a
  known `PublicKey`.
- `enqueue_invoice_using_reply_paths`: For sending an invoice over a
  set of explicitly provided `BlindedMessagePath`s.

This separation improves clarity, enables reuse in broader contexts,
and lays the groundwork for more composable invoice handling across the
Offers/Refund flow.
Adds an API to send an `InvoiceError` to the counterparty via the flow.

This becomes useful with the introduction of Flow events in upcoming
commits, where the user can choose to either respond to Offers Messages
or return an `InvoiceError`.

Note:
Given the small scope of changes in this commit, we also take the
opportunity to perform minor documentation cleanups in `flow.rs`.
Until now, offers messages were processed internally without exposing
intermediate steps. This made it harder for callers to intercept or
analyse offer messages before deciding how to respond to them.

`FlowEvents` provide an optional mechanism to surface these events back
to the user. With events enabled, the caller can manually inspect an
incoming message, choose to construct and sign an invoice, or send back
an InvoiceError. This shifts control to the user where needed, while
keeping the default automatic flow unchanged.
@shaavan
Copy link
Member Author

shaavan commented Oct 11, 2025

@shaavan shaavan requested a review from jkczyz October 11, 2025 19:10
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 3rd Reminder

Hey @jkczyz! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants